@zvndev/darkmown 2.10.1 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/CHANGELOG.md +7 -0
- package/README.md +35 -1
- package/docs/cli.md +19 -0
- package/docs/errors.md +255 -0
- package/docs/spec-alignment.md +5 -0
- package/package.json +2 -1
- package/site/pages/docs/index.wd +6 -1
- package/site/pages/index.wd +1 -0
- package/site/pages/playground/index.js +28 -27
- package/src/builder.js +118 -11
- package/src/catalog.js +6 -0
- package/src/cli.js +35 -0
- package/src/compiler/page.js +16 -4
- package/src/compiler.js +1 -1
- package/src/mcp-tools.js +354 -0
- package/src/mcp.js +287 -0
- package/src/statics.js +1 -0
- package/types/builder.d.ts +5 -0
- package/types/compiler/page.d.ts +12 -3
- package/types/compiler.d.ts +4 -1
package/AGENTS.md
CHANGED
|
@@ -44,7 +44,7 @@ package.json
|
|
|
44
44
|
- **Responsive image variants:** with sharp installed (`npm i -D sharp`, an optional dependency; builds work without it), the build scans `site/pages` for raster photos (jpg/jpeg/png/webp) over 700px wide and writes resized `name@640.ext` / `name@1280.ext` variants into `dist`, wiring a `srcset` onto the Markdown image with the original kept as `src`. Only widths below the intrinsic size are generated (never upscale), variants keep the source format, and EXIF orientation is applied. **Author-provided `name@<width>.ext` exports win per width**: your file is copied verbatim and measured from its own header (a `@640` file holding 600px emits `600w`), and generation fills only the uncovered widths; other sizes are hand exports on the probe list (64, 96, 128, …, 3840). Skipped silently: images ≤700px, `@width` files, hidden/symlinked paths, svg/gif/avif, and shelf images (the generator covers page-colocated images only). Without sharp the build behaves as before plus one `npm i -D sharp` hint per build; a corrupt image or a sharp failure on one file is a hint naming the file, never a build failure. Resizes cache in `node_modules/.cache/darkmown-images/` (content hash + width + format), so rebuilds and dev reloads do zero sharp work for unchanged images. Never wired: bound srcs (``), remote/`data:`/query-hash srcs, page-relative or extensionless srcs, and raw-HTML `<img>` tags; `sizes` is never emitted (raw `<img srcset sizes>` on an `html: true` page is the way). `.md` pages get the width/height/lazy hardening but no srcset. The first image on the page stays eager with `fetchpriority="high"`, the rest lazy-load, and author-set attributes always win.
|
|
45
45
|
- Shelf `.json` files publish to `/__wd/data/<name>`; every other shelf file publishes to `/__wd/media/<path>`. Hidden segments (`.`, `-`, `_`) and symlinks are never published, matching the page-asset rule; a symlink that stays inside `site/_/` still works as an `@include`/`@loop` source (it unrolls into pages at build time), it is just never copied to `dist`.
|
|
46
46
|
|
|
47
|
-
Commands: `darkmown dev`, `darkmown build [--drafts]`, `darkmown validate` (compile-check without writing `dist`), `darkmown serve` (previews `dist`, runs `api/` functions like dev, and compresses text responses), `darkmown deploy <vercel|cloudflare>`, `darkmown catalog [--llms|--llms-full]
|
|
47
|
+
Commands: `darkmown dev`, `darkmown build [--drafts]`, `darkmown validate` (compile-check without writing `dist`), `darkmown serve` (previews `dist`, runs `api/` functions like dev, and compresses text responses), `darkmown deploy <vercel|cloudflare>`, `darkmown catalog [--llms|--llms-full]`, `darkmown mcp` (an MCP server on stdio: catalog, grammar, validate, error codes, docs).
|
|
48
48
|
|
|
49
49
|
## Frontmatter: the complete key set
|
|
50
50
|
|
|
@@ -81,7 +81,7 @@ rss_limit: 20
|
|
|
81
81
|
- `defaults: [name="value"]` (include files only) sets fallback values for `with` arguments; see Includes & sections.
|
|
82
82
|
- `date` marks the page a post (RSS item, sitemap `lastmod`, `og:type=article`). `updated` and `excerpt` refine that. `author` feeds an article `schema:`.
|
|
83
83
|
- `schema:` emits JSON-LD. One value, or a list. The closed set: `Article`, `BlogPosting`, `TechArticle`, `WebSite`, `Organization`. `organization:` and `logo:` support the `Organization` type. Anything else is a compile error.
|
|
84
|
-
- **Home page only:** `site_url: https://example.com` (no trailing slash) turns on `sitemap.xml`, `rss.xml`, and canonical URLs. `ai_crawlers: allow` (default) or `deny` flips every AI-crawler group in `robots.txt`. `rss_limit: 50` sets how many newest posts `rss.xml` carries (default 20).
|
|
84
|
+
- **Home page only:** `site_url: https://example.com` (no trailing slash) turns on `sitemap.xml`, `rss.xml`, and canonical URLs. `ai_crawlers: allow` (default) or `deny` flips every AI-crawler group in `robots.txt`. `rss_limit: 50` sets how many newest posts `rss.xml` carries (default 20). `md_source: true` publishes every page's byte-exact markdown source at `<route>.md` (with a `<link rel="alternate" type="text/markdown">` head tag); a page opts out with `md_source: false`.
|
|
85
85
|
- The page's own frontmatter is in scope as `meta`. Anything else you write is a free-form field, queryable when the page is a collection entry.
|
|
86
86
|
|
|
87
87
|
## Interpolation — one syntax
|
|
@@ -714,6 +714,8 @@ Codes are grouped by subsystem: `WD0xx` source and frontmatter, `WD1xx` loops an
|
|
|
714
714
|
|
|
715
715
|
The ones you are most likely to hit while writing new pages: `WD191` (a reactive `@loop` over `|` table rows), `WD250` (a reserved declaration name), `WD251` (`:state b = a` where `a` is state), `WD260` (`from-url` on `:store`/`:theme`), `WD261` (`persist` and `ephemeral` together), `WD315` (`:every`/`:effect` inside a reactive loop), `WD450` (a bound field with no state), `WD451` (a bound `:checkbox` with several options), `WD452` (a file field in a GET form), `WD650`/`WD651` (a non-whitelisted or unquoted attribute), `WD804`-`WD807` (a malformed `@font-face` or `@keyframes` block: a stray name on `@font-face`, a missing or invalid `@keyframes` name, a non-keyframe child, or a keyframe with no declarations), `WD950` (a bad `rss_limit`). Every `Use:` hint that contains `[bracket]` placeholders ends with a concrete `— e.g. <valid line>`: **copy the example, never the placeholder.** `darkmown catalog --llms-full` prints every code with its cause and fix.
|
|
716
716
|
|
|
717
|
+
**MCP server.** `darkmown mcp` speaks the Model Context Protocol on stdio (protocol 2024-11-05, hand-rolled, zero-dependency; stdout is protocol frames only, logs go to stderr). Five tools: `catalog`, `grammar`, `validate` (compile-check `.wd` source with coded errors), `error` (a WD code's cause, fix, and example), and `doc` (fuzzy section search over the shipped README/AGENTS.md), plus `darkmown://docs/{readme,agents,changelog,errors,cli}` resources. Point a client at it with `npx -y @zvndev/darkmown mcp`.
|
|
718
|
+
|
|
717
719
|
## Hard rules — do not break these
|
|
718
720
|
|
|
719
721
|
1. **Never invent directives or syntax.** These do NOT exist: `@section`/`@endsection`, `{% if %}`, `:for`, `@repeat`, `v-if`, JSX, or any directive not in the list above. The only container is `::: … :::`; the only loop is `@loop`; the only interpolation is `{ }`.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
|
|
4
4
|
|
|
5
|
+
## 2.11.0 (2026-10-15)
|
|
6
|
+
|
|
7
|
+
A release for the agents reading the docs, not just the agents writing pages: the compiler and the whole language description are now one MCP server away, and any page can serve its own markdown source. No breaking change.
|
|
8
|
+
|
|
9
|
+
- **`darkmown mcp`: a zero-dependency MCP server on stdio.** MCP over stdio is newline-delimited JSON-RPC 2.0 (protocol revision 2024-11-05), so the server is hand-rolled on Node built-ins rather than the SDK: `dependencies` stays empty, and there is no SDK version to drift against. stdout carries protocol frames only; everything else goes to stderr. Five tools, each a thin adapter over a surface the CLI and the test suite already pin, so an MCP client and the CLI can never disagree: `catalog` (the directive catalog as `json`, `llms`, or `llms-full`), `grammar` (the full GBNF grammar or a cheatsheet slice), `validate` (compile `.wd`/`.md` source in memory; success answers static/reactive status and warnings, failure answers the WD code, file:line, `Use:` hint, and example), `error` (one code's cause, fix, and example, or the valid list), and `doc` (fuzzy section search over the shipped README and AGENTS.md). Five resources expose the shipped docs at `darkmown://docs/{readme,agents,changelog,errors,cli}`, read from the installed package at request time. Prompts, logging, completion, sampling, subscriptions, and batched requests are deliberately unimplemented (a JSON array gets one Invalid Request error, which the spec permits). Client config: `{ "mcpServers": { "darkmown": { "command": "npx", "args": ["-y", "@zvndev/darkmown", "mcp"] } } }`. As part of the MCP work, **`docs/errors.md` now ships in the published tarball** (it was repo-only before), so the `darkmown://docs/errors` resource works from an installed package.
|
|
10
|
+
- **Per-route markdown sources: `md_source:`.** `md_source: true` on the HOME page opts the whole site in (off by default; a site that never sets it builds byte-identical output). Every route then serves its byte-exact source file one level up from its HTML: `/` answers `/index.md`, `/docs/` answers `/docs.md`, with `text/markdown; charset=utf-8` (a compressible text type, so `darkmown serve` negotiates br/gzip on it), frontmatter and CRLF endings included. Each published page carries `<link rel="alternate" type="text/markdown" href="/docs.md" title="Markdown source">`, emitted exactly when the file is, so it never 404s. A page opts back out with `md_source: false`; a page-level `true` without the home flag does nothing; drafts never emit a source; sitemap, RSS, and robots never list the `.md` files (they are alternates, not routes); paginated listings publish under every generated page (`/news.md` and `/news/page/2.md`). Dev handles the edges: flipping the home flag forces a full rebuild, editing a page refreshes its copy, opting a page out withdraws its copy, and a shrunk pagination removes stale `/page/N.md` files.
|
|
11
|
+
|
|
5
12
|
## 2.10.1 (2026-10-14)
|
|
6
13
|
|
|
7
14
|
A patch release of hardening and contract repair: a security pass over everything the build publishes and serves, the skin at-rule errors gaining their registry codes, a compiler hint for secret-shaped state seeds, and a polish sweep over the playground, templates, and VS Code extension. Behavior tightening an author could notice is called out per item.
|
package/README.md
CHANGED
|
@@ -86,6 +86,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
|
|
|
86
86
|
- `darkmown deploy <vercel|cloudflare> [--prod]` builds and deploys via the platform CLI.
|
|
87
87
|
- `darkmown serve` previews the built `dist` locally. It also runs the project's `api/` functions (same handler contract and routing as `darkmown dev` and the production hosts, so `:form action="/api/…"` posts and `:fetch … from "/api/…"` demos work in the preview), and it compresses text responses: brotli preferred, then gzip, negotiated off the request's `Accept-Encoding` with `Vary: Accept-Encoding` set. `darkmown dev` is deliberately uncompressed.
|
|
88
88
|
- `darkmown catalog [--llms|--llms-full]` prints the machine-readable directive catalog (JSON), a compact cheatsheet with `--llms`, or the complete reference including every error code with `--llms-full`. See [AI authoring](#ai-authoring).
|
|
89
|
+
- `darkmown mcp` runs a zero-dependency MCP server on stdio, exposing the catalog, grammar, validation, error codes, and docs as agent tools. See [MCP server](#mcp-server-darkmown-mcp).
|
|
89
90
|
- `darkmown version` prints the installed package version.
|
|
90
91
|
- `darkmown help` prints CLI usage.
|
|
91
92
|
|
|
@@ -285,6 +286,8 @@ And one key moves styling into frontmatter: **`skins: [/base.skin, /marketing.sk
|
|
|
285
286
|
|
|
286
287
|
A few reserved keys drive **drafts and feeds** (see [SEO & feeds](#seo--feeds-sitemap-rss-robots) below): `draft: true` excludes a page from production builds; `site_url` on the home page turns on `sitemap.xml` + `rss.xml`; `rss_limit:` on the home page caps how many posts the feed carries; `date:` marks a page as a blog post (it lands in `rss.xml` and sets the page's `<lastmod>`); `excerpt:` is the RSS summary. The same frontmatter is queryable when the page is an entry in a [content collection](#content-collections): `{ post.date }`, `{ post.excerpt }`, `{ post.tags }`, and any custom key resolve in an `@loop` over the folder.
|
|
287
288
|
|
|
289
|
+
**Markdown sources: `md_source: true`** on the home page opts the whole site in to publishing every page's byte-exact source beside its HTML: `/docs/` also answers `/docs.md` (the exact `.md`/`.wd` file the author wrote, frontmatter and CRLF endings included) with a `text/markdown` content type, and each published page carries `<link rel="alternate" type="text/markdown" href="/docs.md" title="Markdown source">`, emitted exactly when the file is, so the link never 404s. A page opts back out with `md_source: false`; a page-level `true` without the home flag does nothing, drafts never emit a source, and sitemap/rss/robots never list the `.md` files (they are alternates, not routes). Off by default, and a site that never sets it builds byte-identical output.
|
|
290
|
+
|
|
288
291
|
`transitions: true` opts a page into **instant, flash-free navigation** — zero JavaScript, all declarative. It emits a directional fade+slide **view transition** for the page swap (old lifts up and out, new rises up and in — replacing the default cross-fade, which left both pages ghosted at ~50 % opacity mid-navigation), plus a **`<script type="speculationrules">` prerender** hint that renders the next same-origin page on hover/pointerdown so the click activates an already-painted page (no white render-gap flash). It honors `prefers-reduced-motion`. Only same-origin pages that both opt in transition; browsers without support — or with page-preloading disabled — navigate normally. Off by default; opt out with `transitions: false`. Mark a link `{.no-prefetch}` to exclude it from prerendering. (Chrome disables prerendering while DevTools is open, so test the built site with DevTools closed.)
|
|
289
292
|
|
|
290
293
|
## Static assets
|
|
@@ -1511,7 +1514,7 @@ Every author-facing compile error opens with a stable `WDxxx` code, so it is sea
|
|
|
1511
1514
|
Use: :state name = value [persist|ephemeral] — e.g. :state count = 0
|
|
1512
1515
|
```
|
|
1513
1516
|
|
|
1514
|
-
The full list lives in [`docs/errors.md`](https://github.com/ZVN-DEV/darkmown/blob/master/docs/errors.md): code, cause, fix, and a compilable example each. (
|
|
1517
|
+
The full list lives in [`docs/errors.md`](https://github.com/ZVN-DEV/darkmown/blob/master/docs/errors.md): code, cause, fix, and a compilable example each. (Since 2.11 it also ships in the published tarball, so the `darkmown://docs/errors` MCP resource and offline readers get the same file; `darkmown catalog --llms-full` prints the same content offline.) Codes are grouped by subsystem:
|
|
1515
1518
|
|
|
1516
1519
|
| Range | Subsystem |
|
|
1517
1520
|
| --- | --- |
|
|
@@ -1580,6 +1583,37 @@ session.call("validate", {});
|
|
|
1580
1583
|
|
|
1581
1584
|
`toolPrompt()` renders the tool list for a system prompt, and `parseToolCall(reply)` reads a call back out of a model's answer (brace-counting and string-aware, because the one thing a Darkmown edit reliably contains is `{ count }`).
|
|
1582
1585
|
|
|
1586
|
+
### MCP server: `darkmown mcp`
|
|
1587
|
+
|
|
1588
|
+
`darkmown mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio, so an MCP-capable agent (Claude Code, Codex, Cursor, …) gets the language description and the compiler as first-class tools instead of shelling out. It is hand-rolled and zero-dependency: MCP over stdio is newline-delimited JSON-RPC 2.0 (revision 2024-11-05), so there is no SDK in `dependencies` and nothing to drift. Every tool is a thin adapter over the same drift-guarded surfaces the CLI uses, so an MCP client and the `darkmown` CLI can never disagree about what compiles or what a WD code means. stdout carries protocol frames only; logs go to stderr.
|
|
1589
|
+
|
|
1590
|
+
| Tool | Answers |
|
|
1591
|
+
| --- | --- |
|
|
1592
|
+
| `catalog` | The directive catalog: `mode "json"` (default), `"llms"` (the cheatsheet), or `"llms-full"` (the complete reference). |
|
|
1593
|
+
| `grammar` | The GBNF directive-line grammar (`gbnf: true`), or a category slice of the cheatsheet (`categories: ["state", "actions"]`). |
|
|
1594
|
+
| `validate` | Compile `.wd`/`.md` source in memory: static/reactive status and warnings on success, the WD code, file:line, `Use:` hint, and example on failure. |
|
|
1595
|
+
| `error` | One WD code's cause, fix, and example; an unknown code answers the valid list. |
|
|
1596
|
+
| `doc` | Fuzzy-find a section of the shipped README/AGENTS.md by topic (`topic: "includes"`; no topic lists every section). |
|
|
1597
|
+
|
|
1598
|
+
Five resources expose the shipped docs at `darkmown://docs/readme`, `darkmown://docs/agents`, `darkmown://docs/changelog`, `darkmown://docs/errors`, and `darkmown://docs/cli`, read from the installed package at request time.
|
|
1599
|
+
|
|
1600
|
+
Client config (Claude Code, Cursor, and friends share this shape):
|
|
1601
|
+
|
|
1602
|
+
```json
|
|
1603
|
+
{
|
|
1604
|
+
"mcpServers": {
|
|
1605
|
+
"darkmown": {
|
|
1606
|
+
"command": "npx",
|
|
1607
|
+
"args": ["-y", "@zvndev/darkmown", "mcp"]
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
```
|
|
1612
|
+
|
|
1613
|
+
Deliberately out of scope: prompts, logging, completion, sampling, subscriptions, and batched requests (a JSON array gets a single Invalid Request error, which the spec permits).
|
|
1614
|
+
|
|
1615
|
+
**Any page as plain markdown.** With [`md_source: true`](#frontmatter) on the home page, every route also serves its byte-exact source at `<route>.md` (`/docs/` answers `/docs.md`), advertised with a `<link rel="alternate" type="text/markdown">` head tag, so an agent can read any page without parsing HTML. `llms-full.txt` already carries the full source text of every page; the `.md` sources are the per-page, always-current form of the same idea.
|
|
1616
|
+
|
|
1583
1617
|
## Editor support
|
|
1584
1618
|
|
|
1585
1619
|
A VS Code extension in [`editors/vscode`](https://github.com/ZVN-DEV/darkmown/tree/master/editors/vscode) gives `.wd` and `.skin` files syntax highlighting, snippets, and folding — so a `.wd` file reads as Markdown-plus-directives, never as broken Markdown.
|
package/docs/cli.md
CHANGED
|
@@ -129,6 +129,25 @@ darkmown catalog --llms-full > reference.md
|
|
|
129
129
|
|
|
130
130
|
Every `darkmown build` also writes both files (`dist/llms.txt` and `dist/llms-full.txt`), and Darkmown ships a generated GBNF grammar (`grammar/wd-directives.gbnf`) for constrained decoding.
|
|
131
131
|
|
|
132
|
+
### `darkmown mcp`
|
|
133
|
+
|
|
134
|
+
Runs a Model Context Protocol server on stdio, so an MCP-capable agent gets the language description and the compiler as first-class tools. It is hand-rolled with zero dependencies: MCP over stdio is newline-delimited JSON-RPC 2.0 (protocol revision 2024-11-05), stdout carries protocol frames only, and logs go to stderr. Every tool is a thin adapter over the same drift-guarded surfaces the CLI uses, so a client and the CLI can never disagree.
|
|
135
|
+
|
|
136
|
+
Five tools: `catalog` (the directive catalog as `json`, `llms`, or `llms-full`), `grammar` (the full GBNF grammar, or a category slice of the cheatsheet), `validate` (compile `.wd`/`.md` source in memory: static/reactive status and warnings on success, the WD code with file:line, `Use:` hint, and example on failure), `error` (one WD code's cause, fix, and example, or the valid list), and `doc` (fuzzy section search over the shipped README and AGENTS.md). Five resources expose the shipped docs at `darkmown://docs/readme`, `darkmown://docs/agents`, `darkmown://docs/changelog`, `darkmown://docs/errors`, and `darkmown://docs/cli`, read from the installed package at request time. Prompts, logging, completion, sampling, subscriptions, and batched requests are deliberately not implemented.
|
|
137
|
+
|
|
138
|
+
Client config (Claude Code, Cursor, and friends share this shape):
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"darkmown": {
|
|
144
|
+
"command": "npx",
|
|
145
|
+
"args": ["-y", "@zvndev/darkmown", "mcp"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
132
151
|
## Backends (`api/`)
|
|
133
152
|
|
|
134
153
|
Backend endpoints are plain-JS Web-standard handlers in a top-level `api/` directory — the same shape on every host, and the only thing you write for a server:
|
package/docs/errors.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Darkmown compile error codes
|
|
2
|
+
|
|
3
|
+
Every Darkmown compile error carries a stable `WDxxx` code. It is the first
|
|
4
|
+
thing in the message (`[WD201] Malformed :state in …`) and it is mirrored on
|
|
5
|
+
the thrown error as `err.wd.code`, alongside `file`, `line`, `hint`, and a
|
|
6
|
+
concrete compilable `example`.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
[WD201] Malformed :state in site/pages/index.wd:1: :state x.
|
|
10
|
+
Use: :state name = value [persist|ephemeral] — e.g. :state count = 0
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Codes are grouped by subsystem, and a shipped code is permanent: it is never
|
|
14
|
+
renumbered and a retired number is never reused. Add new errors at the next
|
|
15
|
+
free number in their block.
|
|
16
|
+
|
|
17
|
+
| Range | Subsystem |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `WD0xx` | Source, frontmatter & block structure |
|
|
20
|
+
| `WD1xx` | Loops & collections |
|
|
21
|
+
| `WD2xx` | State & expressions |
|
|
22
|
+
| `WD3xx` | Button, effect & timer actions |
|
|
23
|
+
| `WD4xx` | Forms & form fields |
|
|
24
|
+
| `WD5xx` | Data fetching & URL safety |
|
|
25
|
+
| `WD6xx` | Includes & page structure |
|
|
26
|
+
| `WD7xx` | Media & embeds |
|
|
27
|
+
| `WD8xx` | Skins & styling |
|
|
28
|
+
| `WD9xx` | Project, routing & CLI |
|
|
29
|
+
|
|
30
|
+
> This page is generated from `src/errors.js` by `node scripts/gen-errors.mjs`.
|
|
31
|
+
> Edit the registry, not this file.
|
|
32
|
+
|
|
33
|
+
## WD0xx — Source, frontmatter & block structure
|
|
34
|
+
|
|
35
|
+
| Code | Error | Cause | Fix | Example |
|
|
36
|
+
| --- | --- | --- | --- | --- |
|
|
37
|
+
| `WD001` | Unterminated frontmatter | The file opens with `---` but never closes the block. | Add a closing `---` on its own line before the page body. | |
|
|
38
|
+
| `WD002` | Disallowed path segment | A dotted path contains `constructor`, `prototype`, or `__proto__`. | Read a different path; those segments are rejected everywhere for safety. | |
|
|
39
|
+
| `WD003` | Cannot interpolate an object value | `{ name }` resolved to an object, which has no sensible text form. | Interpolate one of its fields (`{ name.title }`) or iterate it with `@loop`. | |
|
|
40
|
+
| `WD004` | Unbalanced JSON value | A `:state`/`:store`/`:theme` value opens `[` or `{` and never closes it. | Balance the literal (no blank lines inside it), or quote it as text. | `:state count = 0` |
|
|
41
|
+
| `WD005` | Loop variable used outside a loop | `{ $index }` (or another `$` row variable) appears outside a `@loop` body. | Move the interpolation into a loop body, or rename your value. | |
|
|
42
|
+
| `WD006` | Unknown format pipe | `{ value \| name }` names a formatter that is not in the fixed whitelist. | Use one of the catalogued pipes; the message lists every available name. | |
|
|
43
|
+
| `WD007` | Stray `:::` close | A bare `:::` line appears with no container open. | Open the container first, or delete the stray closer. | `::: card .featured` |
|
|
44
|
+
| `WD008` | Retired `@repeat` directive | `@repeat` was replaced by the single `@loop` directive. | Rewrite the block as `@loop … into … @endloop`. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
45
|
+
| `WD009` | Retired `:for` directive | `:for` was replaced by the single `@loop` directive. | Rewrite the block as `@loop … into … @endloop`. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
46
|
+
| `WD010` | Stray block closer | An `@endloop`/`:endif`/`:endform`/`:else` line has no matching opener. | Open the matching block, or delete the stray closer. | |
|
|
47
|
+
| `WD011` | Missing block closer | A `@loop`/`:form`/`:carousel` block runs to the end of the file unclosed. | Close the block with its own closing token. | |
|
|
48
|
+
| `WD012` | Missing closing `:::` | A `::: container` block is never closed. | Add a bare `:::` line to close the container. | `::: card .featured` |
|
|
49
|
+
| `WD013` | `:else if` after `:else` | A conditional continues with `:else if` after a bare `:else` already closed it. | Order every `:else if` before the bare `:else`. | |
|
|
50
|
+
| `WD014` | Duplicate `:else` | A conditional has more than one bare `:else` branch. | Keep one `:else`; turn the others into `:else if` branches. | |
|
|
51
|
+
| `WD015` | Missing `:endif` | An `:if` region is never closed. | Close the region with `:endif`. | `:if count > 0` |
|
|
52
|
+
| `WD016` | Unknown structured-data type | Frontmatter `schema:` names a type outside the supported whitelist. | Use one of the listed schema.org types; the message names every one. | |
|
|
53
|
+
| `WD017` | Invalid `skins:` frontmatter entry | A `skins:` entry is missing, is not a `.skin` file, escapes `site/pages`/`site/_`, or the value is not an inline array of paths. | Write `skins: [/base.skin]` — an inline array of .skin paths inside site/pages or site/_. | |
|
|
54
|
+
|
|
55
|
+
## WD1xx — Loops & collections
|
|
56
|
+
|
|
57
|
+
| Code | Error | Cause | Fix | Example |
|
|
58
|
+
| --- | --- | --- | --- | --- |
|
|
59
|
+
| `WD101` | Malformed `@loop` header | The header does not read `@loop <source> into <item>`. | Name a source and an item variable, then any clauses in the fixed order. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
60
|
+
| `WD102` | Malformed `@loop` clause | A clause is misspelled or written out of the fixed order. | Order clauses `where`, `sort by`, `reverse`, `offset`, `limit`, `paginate`, `sortable`. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
61
|
+
| `WD103` | Unknown `sort by { state }` key | A reactive sort key references a `:state`/`:store` that is not declared. | Declare the state first, or sort by a literal item field. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
62
|
+
| `WD104` | Sort key is not a loop-item field | `sort by` names something other than the loop item. | Sort by `<item>.field`, using the item name from the `into` clause. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
63
|
+
| `WD105` | Disallowed sort key path | The sort key contains `constructor`, `prototype`, or `__proto__`. | Sort by an ordinary data field. | |
|
|
64
|
+
| `WD106` | Unknown sort direction state | A reactive `{ direction }` references a `:state`/`:store` that is not declared. | Declare the state first, or use the literal `asc`/`desc`. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
65
|
+
| `WD107` | Invalid `offset`/`limit` argument | The argument is neither a non-negative integer nor a declared `:state`. | Pass an integer literal or a declared state key. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
66
|
+
| `WD108` | `sortable` combined with other clauses | `sortable` reorders the underlying list, so a derived view would desynchronise. | Drop `where`/`sort`/`reverse`/`offset`/`limit`/`paginate` from a sortable loop. | |
|
|
67
|
+
| `WD109` | `paginate` combined with `offset`/`limit` | `paginate` already slices each page, so an explicit slice conflicts with it. | Keep `paginate N` and remove `offset`/`limit`. | |
|
|
68
|
+
| `WD110` | `paginate` on a non-collection source | Pagination multiplies static routes, which only makes sense for a collection. | Paginate a collection (a `site/pages/<name>/` subdirectory) by its bare name. | |
|
|
69
|
+
| `WD111` | Loop data file is not a JSON array | The `.json` source parses to an object or a scalar. | Make the file a top-level JSON array of rows. | |
|
|
70
|
+
| `WD112` | In-scope loop source is not a list | The name resolved to a scalar or object value in scope. | Point the loop at a list, or omit the field to render the `@empty` branch. | |
|
|
71
|
+
| `WD113` | Disallowed `@loop` source path | The source path contains `constructor`, `prototype`, or `__proto__`. | Loop an ordinary collection, JSON file, scope value, or state list. | |
|
|
72
|
+
| `WD114` | `sortable` on a nested loop | An item-relative loop has no top-level state list to reorder. | Make the sortable loop read a top-level `:state`/`:store` list. | |
|
|
73
|
+
| `WD115` | Unresolved `@loop` source | The source is not a collection, JSON file, in-scope value, or declared state. | Check the name; the message lists the available collections. | `@loop /products.json into p where p.price < 50 sort by p.price asc` |
|
|
74
|
+
| `WD116` | Reactive loop nested too deep | A third reactive `@loop` level would paint empty at runtime. | Unroll the outer data at build time, or move the innermost list into build data. | |
|
|
75
|
+
| `WD117` | `sortable` needs a state list | `sortable` was used on a JSON file or in-scope value, which cannot be reordered. | Reorder a `:state`/`:store` list instead. | |
|
|
76
|
+
| `WD120` | Malformed `_schema.wd` line | A schema line does not read `field: type`. | Write one `field: type` rule per line inside the `---` block. | |
|
|
77
|
+
| `WD121` | Unknown `_schema.wd` type | The type is outside the closed vocabulary. | Use `string`, `number`, `boolean`, `date`, or `string[]`, with `?` for optional. | |
|
|
78
|
+
| `WD122` | Undeclared frontmatter field | A collection entry has a field the schema does not declare (often a typo). | Add the field to `_schema.wd`, or remove it from the entry. | |
|
|
79
|
+
| `WD123` | Missing required frontmatter field | A collection entry omits a field the schema requires. | Add the field to the entry, or mark it optional with `?` in the schema. | |
|
|
80
|
+
| `WD124` | Frontmatter field has the wrong type | An entry's value does not match its schema type. | Fix the entry's value, or widen the type in `_schema.wd`. | |
|
|
81
|
+
| `WD190` | Expression could not be compiled | A `:if` or `::: … when` condition folded a build-time value the expression re-parser cannot read back. | Use simpler operands — a field path, a declared `:state`, a plain number, or a `"string"`. | |
|
|
82
|
+
| `WD191` | Reactive `@loop` over markdown table rows | A `@loop` whose body is bare `\| … \|` cells resolves to a reactive source, and a reactive row is cloned into a `<div>`, which cannot live inside a `<table>`. | Loop a static source (a JSON file, a frontmatter list, or a collection) for a markdown table — and if the source is already static, drop the `:state` the `where`/`limit`/`sort by` clause reads — or build reactive rows from containers (`::: trow` / `::: td`) instead of `\|` cells. | |
|
|
83
|
+
|
|
84
|
+
## WD2xx — State & expressions
|
|
85
|
+
|
|
86
|
+
| Code | Error | Cause | Fix | Example |
|
|
87
|
+
| --- | --- | --- | --- | --- |
|
|
88
|
+
| `WD201` | Malformed `:state` | The line does not read `:state name = value [persist\|ephemeral]`. | Give the state a name and an initial value. | `:state count = 0` |
|
|
89
|
+
| `WD202` | `:state` inside a reactive loop | State cannot be declared per row; a loop body has no place to hold it. | Declare the state outside the loop. | |
|
|
90
|
+
| `WD203` | `:state` collides with a `:store` | A page-global store already owns that name. | Rename one of them, or use the store everywhere. | `:store cart = []` |
|
|
91
|
+
| `WD204` | `:state` declared twice | The same name is declared twice in one section scope. | Remove the duplicate, or scope one declaration to its own `::: section`. | |
|
|
92
|
+
| `WD205` | Malformed `:store` | The line does not read `:store name = value [persist\|ephemeral]`. | Give the store a name and an initial value. | `:store cart = []` |
|
|
93
|
+
| `WD206` | `:store` declared twice | Stores are page-global, so a second declaration is ambiguous. | Declare the store once and reference it everywhere else. | `:store cart = []` |
|
|
94
|
+
| `WD207` | `:store` collides with a `:state` | A `:state` of the same name is already declared on the page. | Rename one of them, or use the store everywhere. | `:store cart = []` |
|
|
95
|
+
| `WD208` | Malformed `:computed` | The line does not read `:computed name = <expression>`. | Give the computed value a name and a right-hand expression. | `:computed total = items.length * 4` |
|
|
96
|
+
| `WD209` | Malformed `:computed` expression | The right-hand side passes the character whitelist but is not a real expression. | Write a complete expression over declared state, numbers, and operators. | `:computed total = items.length * 4` |
|
|
97
|
+
| `WD210` | Malformed `:theme` | The line does not read `:theme [name] [= "auto"]`. | Use a bare `:theme`, or name the store and seed it. | `:theme` |
|
|
98
|
+
| `WD211` | Persistence token on `:computed` | The expression ends in a bare `persist`, `ephemeral`, or `from-url`, which is swallowed into it. | Computed values are derived, not stored. Put the token on the state they derive from instead. | `:computed total = items.length * 4` |
|
|
99
|
+
| `WD220` | Malformed `where` condition | A `where` condition is not `operand <op> operand`. | Compare a loop-item field with a value using a whitelisted operator. | |
|
|
100
|
+
| `WD221` | Unsupported `where` operand | An operand is not an item field, a state name, a number, or a quoted string. | Use one of those four operand forms. | |
|
|
101
|
+
| `WD222` | Disallowed `where` path | An operand path contains `constructor`, `prototype`, or `__proto__`. | Compare an ordinary data field. | |
|
|
102
|
+
| `WD223` | Unknown name in `where` | An operand names neither the loop item nor a declared `:state`. | Use the loop item's field, or declare the state first. | |
|
|
103
|
+
| `WD224` | Unsupported `when`/`:if` operand | An operand is not an item field, a state name, a number, or a quoted string. | Use one of those four operand forms. | `:if count > 0` |
|
|
104
|
+
| `WD225` | Disallowed `when`/`:if` path | An operand path contains `constructor`, `prototype`, or `__proto__`. | Compare an ordinary data field. | |
|
|
105
|
+
| `WD226` | Unknown name in `when`/`:if` | An operand names nothing in scope: no loop item, no scope value, no state. | Declare the state first, or reference a value that is in scope. | `:if count > 0` |
|
|
106
|
+
| `WD227` | Disallowed aggregate path | A `sum`/`avg`/`min`/`max`/`count` argument contains a prototype-poisoning segment. | Aggregate an ordinary state list. | |
|
|
107
|
+
| `WD228` | Unknown state in a `:computed` aggregate | The aggregated list is not a declared `:state`/`:store`/`:fetch` key. | Declare the list first. | `:computed total = items.length * 4` |
|
|
108
|
+
| `WD229` | Unsupported string syntax in `:computed` | The expression contains a quote, backslash, or backtick outside a simple literal. | Use plain double- or single-quoted string literals. | |
|
|
109
|
+
| `WD230` | Unsupported syntax in `:computed` | The expression contains characters outside the closed whitelist. | Use state names, numbers, strings, arithmetic, comparisons, and `&& \|\| !`. | `:computed total = items.length * 4` |
|
|
110
|
+
| `WD231` | Assignment in `:computed` | A `:computed` expression derives a value; it may not assign one. | Mutate state from a `:button`/`:effect` action instead. | |
|
|
111
|
+
| `WD232` | Function call in `:computed` | Only the fixed `sum`/`avg`/`min`/`max`/`count` aggregates may look like calls. | Remove the call, or use one of the aggregates. | |
|
|
112
|
+
| `WD233` | Disallowed `:computed` path segment | A referenced path contains `constructor`, `prototype`, or `__proto__`. | Read an ordinary state path. | |
|
|
113
|
+
| `WD234` | Unknown state in `:computed` | The expression references a name that is not declared state. | Declare it with `:state`, `:store`, or `:fetch` first. | `:computed total = items.length * 4` |
|
|
114
|
+
| `WD250` | Reserved state declaration name | A `:state`/`:store`/`:computed`/`:fetch`/`:form`/`:slider`/`:theme` name is `__proto__`, `constructor`, or `prototype`, which the runtime's state object inherits rather than owns. | Rename the key — any other name works. | `:state count = 0` |
|
|
115
|
+
| `WD251` | State seeded from another state | A `:state`/`:store` value is a bare name that is already declared state, so it would be stored as that literal text and never track the value. | Derive it with `:computed`, or quote the text to keep it literal. | `:computed total = items.length * 4` |
|
|
116
|
+
| `WD260` | `from-url` on a keyword that cannot take it | `:store` and `:theme` are shared by every page and every tab, while a query parameter belongs to one page's address, so the two cannot mean the same thing. | Declare the value with `:state … from-url`, or drop the token. | `:state q = "" from-url` |
|
|
117
|
+
| `WD261` | Conflicting persistence modifiers | A declaration carries `persist` and `ephemeral` at once, or repeats the same modifier. | Pick one: `persist` keeps the value across reloads, `ephemeral` drops it. | `:state count = 0` |
|
|
118
|
+
|
|
119
|
+
## WD3xx — Button, effect & timer actions
|
|
120
|
+
|
|
121
|
+
| Code | Error | Cause | Fix | Example |
|
|
122
|
+
| --- | --- | --- | --- | --- |
|
|
123
|
+
| `WD301` | Malformed `:button` | The line does not read `:button "Label" -> action`. | Quote the label and give at least one action after the arrow. | `:button "Add one" -> count++` |
|
|
124
|
+
| `WD302` | Malformed `:effect` | The line does not read `:effect watchedState -> action`. | Name one state to watch and the actions to run when it changes. | `:effect query -> searches++` |
|
|
125
|
+
| `WD303` | `:effect` watches unknown state | The watched name is not a declared `:state`/`:store`. | Declare the state first. | `:effect query -> searches++` |
|
|
126
|
+
| `WD304` | Malformed `:every` | The line does not read `:every <duration> -> action`. | Give a duration and the actions to run on each tick. | `:every 5s -> seconds++` |
|
|
127
|
+
| `WD305` | Invalid `:every` duration | The duration is not a positive `<int>[ms\|s\|m]` value. | Use a duration like `500ms`, `5s`, or `2m`. | `:every 5s -> seconds++` |
|
|
128
|
+
| `WD306` | Action targets unknown state | The action mutates a name that is not declared state. | Declare the state first. | `:button "Add one" -> count++` |
|
|
129
|
+
| `WD307` | Row append needs a list state | `list += item` carries the current row into a target that is not a list. | Declare the target as a list, e.g. `:state cart = []`. | |
|
|
130
|
+
| `WD308` | `+=` needs a number or a list | `+=` with a non-number value requires a list target to append to. | Add a number, or declare the target as a list. | |
|
|
131
|
+
| `WD309` | Row remove inside a nested loop | An item-relative loop has no top-level list for the runtime to remove from. | Carry the row into a top-level list first, then remove it there. | |
|
|
132
|
+
| `WD310` | Row remove targets the wrong list | Per-row `remove` must target the same list the loop is iterating. | Name the looped list, or remove a value instead of the current row. | |
|
|
133
|
+
| `WD311` | Unsupported action | The expression is outside the fixed action vocabulary (no JavaScript is allowed). | Use a catalogued op; the message lists the whole vocabulary. | `count++` |
|
|
134
|
+
| `WD312` | `merge` operand is unknown state | The right-hand name of a `merge` is not declared state. | Declare it first, or pass an inline object literal. | |
|
|
135
|
+
| `WD313` | Unsupported `merge` operand | The operand is neither a state key nor an inline object literal. | Merge a declared object state, or an inline `{…}` literal. | |
|
|
136
|
+
| `WD314` | Unsupported action literal | The action value is not a quoted string, number, boolean, null, or valid JSON. | Quote strings and use valid JSON for arrays and objects. | |
|
|
137
|
+
| `WD315` | `:every`/`:effect` inside a reactive `@loop` | A reactive loop clones its body per row, so the timer or watcher would be registered once per row and a removed row's would keep firing. | Declare it once outside the loop — at page level or inside the `:::` section — and act on the whole list. | `:every 5s -> seconds++` |
|
|
138
|
+
|
|
139
|
+
## WD4xx — Forms & form fields
|
|
140
|
+
|
|
141
|
+
| Code | Error | Cause | Fix | Example |
|
|
142
|
+
| --- | --- | --- | --- | --- |
|
|
143
|
+
| `WD401` | Malformed `:form` | The opener has neither `into <name>` nor `action="…"`, or has leftover text. | Capture into state, post to a URL, or do both for a fetch round-trip. | `:form into contact` |
|
|
144
|
+
| `WD402` | Malformed `:input` | The line does not start with a field name. | Name the field, then add optional attributes and flags. | `:input email type=email required` |
|
|
145
|
+
| `WD403` | Unknown `:input` flag | The bare flag is not one of `required`/`autofocus`/`disabled`/`readonly`. | Use a supported flag, or write it as `name=value`. | `:input email type=email required` |
|
|
146
|
+
| `WD404` | Unknown `:input` attribute | The attribute is outside the supported set. | Use a supported attribute; the compiler emits no arbitrary HTML attributes. | `:input email type=email required` |
|
|
147
|
+
| `WD405` | Malformed `:bind` | The line does not start with the bound state name. | Name a declared `:state`, then add optional attributes. | `:bind query placeholder="Search"` |
|
|
148
|
+
| `WD406` | `:bind` has no matching state | The bound name is not a declared `:state`/`:store`. | Declare the state before binding to it. | `:bind query placeholder="Search"` |
|
|
149
|
+
| `WD407` | Unknown `:bind` flag | The bare flag is not `required` or `autofocus`. | Use a supported flag. | `:bind query placeholder="Search"` |
|
|
150
|
+
| `WD408` | Unknown `:bind` attribute | The attribute is outside the supported set. | Use `placeholder`, `type`, `autocomplete`, or an `aria-*` attribute. | `:bind query placeholder="Search"` |
|
|
151
|
+
| `WD409` | Malformed `:slider` | The line does not start with the slider's state name. | Name the state, then optionally seed it and set the range. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
152
|
+
| `WD410` | Malformed `:slider` initial value | The `=` is present but no value follows it. | Give the slider a numeric initial value. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
153
|
+
| `WD411` | Unknown `:slider` attribute | The attribute is not `min`, `max`, `step`, or `aria-label`. | Use a supported attribute. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
154
|
+
| `WD412` | Non-numeric `:slider` bound | `min`, `max`, or `step` is not a number. | Give every bound a numeric value. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
155
|
+
| `WD413` | Non-numeric `:slider` initial value | A slider is bound to a number, so its seed must be numeric. | Seed the slider with a number. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
156
|
+
| `WD414` | `:slider persist` without an inline value | `persist` belongs to the declaration, and this slider binds existing state. | Declare the value inline to persist it, or persist it on the `:state` line. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
157
|
+
| `WD415` | `:slider` has no matching state | The slider binds a name that is not declared state. | Declare the state, or seed it inline on the slider. | `:slider volume = 50 min=0 max=100 step=1` |
|
|
158
|
+
| `WD416` | Malformed `:submit` | The line does not read `:submit "Label"`. | Quote the button label. | `:submit "Send"` |
|
|
159
|
+
| `WD417` | Malformed `:textarea` | The line does not start with a field name. | Name the field, then add optional attributes and flags. | `:textarea message placeholder="Your message" rows=4` |
|
|
160
|
+
| `WD418` | Unknown `:textarea` flag | The bare flag is not one of `required`/`autofocus`/`disabled`/`readonly`. | Use a supported flag. | `:textarea message placeholder="Your message" rows=4` |
|
|
161
|
+
| `WD419` | Unknown `:textarea` attribute | The attribute is outside the supported set. | Use a supported attribute such as `placeholder`, `rows`, or `maxlength`. | `:textarea message placeholder="Your message" rows=4` |
|
|
162
|
+
| `WD420` | Malformed `:select` | The line does not start with a field name. | Name the field, then list the options as `- Label` lines. | `:select topic` |
|
|
163
|
+
| `WD421` | Unknown `:select` flag | The bare flag is not `required`, `disabled`, or `autofocus`. | Use a supported flag. | `:select topic` |
|
|
164
|
+
| `WD422` | Unknown `:select` attribute | The attribute is not `autocomplete` or an `aria-*` attribute. | Use a supported attribute. | `:select topic` |
|
|
165
|
+
| `WD423` | `:select` has no options | No `- Label` lines follow the opener. | Add one `- Label` line per option directly beneath it. | `:select topic` |
|
|
166
|
+
| `WD424` | Malformed `:checkbox`/`:radio` | The line does not start with a group name. | Name the group, then list the options as `- Label` lines. | `:checkbox toppings` |
|
|
167
|
+
| `WD425` | Unknown `:checkbox`/`:radio` flag | The bare flag is not `required`, `disabled`, or `autofocus`. | Use a supported flag. | `:checkbox toppings` |
|
|
168
|
+
| `WD426` | Unknown `:checkbox`/`:radio` attribute | The attribute is not `aria-label` or `aria-describedby`. | Use a supported attribute. | `:checkbox toppings` |
|
|
169
|
+
| `WD427` | `:checkbox`/`:radio` has no options | No `- Label` lines follow the opener. | Add one `- Label` line per option directly beneath it. | `:radio size` |
|
|
170
|
+
| `WD450` | Bound field with no state | A `:select`/`:radio`/`:checkbox` outside a `:form` names state that is not declared, so there is nothing for it to bind to. | Declare the state first, or move the field inside a `:form`, where the name is a form field instead. | `:select topic` |
|
|
171
|
+
| `WD451` | Bound `:checkbox` with several options | A `:checkbox` bound to state carries one true/false, so a list of options has no meaning. | Give it one `- Label` line, or use a `:radio` group for a set of choices. | `:checkbox toppings` |
|
|
172
|
+
| `WD452` | File field in a GET form | A `:form` contains an `<input type=file>` and declares `method="get"`. A GET request has no body, so only the file's name would travel. | Drop `method="get"` — a `:form` posts by default, and a form with a file field is submitted as multipart. | `:form into contact` |
|
|
173
|
+
|
|
174
|
+
## WD5xx — Data fetching & URL safety
|
|
175
|
+
|
|
176
|
+
| Code | Error | Cause | Fix | Example |
|
|
177
|
+
| --- | --- | --- | --- | --- |
|
|
178
|
+
| `WD501` | Malformed `:fetch` | The line does not read `:fetch name from "url" [options]`. | Name the state key and quote the URL, then add keyword options. | `:fetch todos from "/api/todos.json" when=visible` |
|
|
179
|
+
| `WD502` | Unknown `:fetch` option | An option is not a `name=value` pair from the supported set. | Use `method`, `when`, `timeout`, `retry`, `headers`, `body`, or `refresh`. | `:fetch todos from "/api/todos.json" when=visible` |
|
|
180
|
+
| `WD503` | Unsupported `:fetch` method | The method is outside `GET`/`POST`/`PUT`/`PATCH`/`DELETE`. | Use one of the supported HTTP methods. | `:fetch todos from "/api/todos.json" when=visible` |
|
|
181
|
+
| `WD504` | Unsupported `:fetch when` | `when` is neither `load` nor `visible`. | Fetch on `load` (the default) or when the marker becomes `visible`. | `:fetch todos from "/api/todos.json" when=visible` |
|
|
182
|
+
| `WD505` | Non-integer `:fetch timeout`/`retry` | The value is not a non-negative integer. | Pass whole numbers (milliseconds for `timeout`, attempts for `retry`). | `:fetch todos from "/api/todos.json" when=visible` |
|
|
183
|
+
| `WD506` | `:fetch refresh=` without `headers=` | A token refresh has nowhere to write the renewed token back to. | Add `headers=<stateKey>` alongside `refresh=`. | `:fetch todos from "/api/todos.json" when=visible` |
|
|
184
|
+
| `WD507` | Unsafe URL | The URL is empty, padded with whitespace, or contains control characters. | Use a clean relative path, an http(s) URL, or a `{ state }` interpolation. | |
|
|
185
|
+
| `WD508` | Protocol-relative URL | A `//host` URL inherits whatever scheme the page was served over. | Write `http://` or `https://` explicitly. | |
|
|
186
|
+
| `WD509` | Disallowed URL scheme | The scheme is not `http` or `https` (`javascript:`, `data:`, `file:`, and friends). | Use http(s) or a relative path. | |
|
|
187
|
+
|
|
188
|
+
## WD6xx — Includes & page structure
|
|
189
|
+
|
|
190
|
+
| Code | Error | Cause | Fix | Example |
|
|
191
|
+
| --- | --- | --- | --- | --- |
|
|
192
|
+
| `WD601` | Include outside the source sandbox | The include path escapes `site/pages` and `site/_`. | Move the partial inside the sandbox and reference it from there. | `@include /header.wd` |
|
|
193
|
+
| `WD602` | Include not found | No file matched the spec in `site/pages` or `site/_`. | Check the path, the extension, and the leading slash. | `@include /header.wd` |
|
|
194
|
+
| `WD603` | Malformed `@include` | The line does not read `@include /partial.wd [with key="value"]`. | Give one target path, then optional `with` arguments. | `@include /header.wd` |
|
|
195
|
+
| `WD604` | Include argument is not in scope | A `with key={ value }` argument references a name that does not resolve. | Pass a literal, or reference a value that is in scope at the include site. | `@include /header.wd` |
|
|
196
|
+
| `WD605` | Unexpected token in a container header | The header holds something that is not a name, `.class`, or `#id`. | Use a leading name, then `.class`/`#id` tokens. | `::: card .featured` |
|
|
197
|
+
| `WD606` | Malformed `:carousel` | The opener carries something other than `autoplay=<ms>`. | Open it bare, or with a numeric `autoplay`. | `:carousel autoplay=3000` |
|
|
198
|
+
| `WD607` | Loop variable in `:if` outside a loop | `:if $first` (or another row variable) appears outside a `@loop` body. | Move the conditional into the loop body. | |
|
|
199
|
+
| `WD608` | `:if` name is not declared | The condition names neither a `:state` nor an in-scope value. | Declare the state first, or reference a value that is in scope. | `:if count > 0` |
|
|
200
|
+
| `WD609` | Malformed `:if` | The `:if` line carries no condition. | Give a name, or a comparison joined with `and`/`or`/`not`. | `:if count > 0` |
|
|
201
|
+
| `WD610` | Unsafe `:try` href | The href has control characters, or a scheme outside http/https/mailto. | Use a relative URL, or an `http:`, `https:`, or `mailto:` URL. | |
|
|
202
|
+
| `WD611` | Protocol-relative `:try` href | A `//host` href inherits whatever scheme the page was served over. | Write `http:` or `https:` explicitly. | |
|
|
203
|
+
| `WD612` | Include cycle | An `@include` chain reaches a file that is already being compiled, so the compile would never terminate. | Break the loop: remove the `@include` that points back, or move the shared content into a third file both sides include. | |
|
|
204
|
+
| `WD613` | Invalid include `defaults` | An include file's `defaults:` frontmatter is not an inline array of `name="value"` entries. | Write an inline array of quoted entries, e.g. `defaults: [title="Untitled"]`. Values are strings; a caller-provided `with` argument always wins over a default. | `@include /header.wd` |
|
|
205
|
+
| `WD614` | Stray `@endinclude` | An `@endinclude` has no unpaired `@include` before it — the closer pairs with the nearest preceding unpaired opener, and every opener in range already paired. | Remove the extra closer, or open a block above it: `@include /partial.wd`, the children lines, then `@endinclude`. | `@include /header.wd` |
|
|
206
|
+
| `WD616` | Include block nested too deep | A block-form `@include … @endinclude` sits inside another include block's children or inside a nested include's own body, past the one supported level. | Keep one level: move the inner block into the include's own file, or use the inline form `@include /partial.wd`. | `@include /header.wd` |
|
|
207
|
+
| `WD650` | Attribute is not on the accessibility whitelist | A `::: ` container header or a `:button` carries an attribute outside the accessibility whitelist (`onclick=`, `style=`, `href=`, `class=`, `data-…=`). | Keep only `role="…"`, `aria-…="…"`, and `title="…"`; style with `.class` tokens and act with `->` actions. | `::: card .note role="region" aria-label="Notes"` |
|
|
208
|
+
| `WD651` | Accessibility attribute has no quoted value | An attribute in a container header or a `:button` is missing its double-quoted value. | Write the value in double quotes, e.g. `role="region"`. | `::: card .note role="region" aria-label="Notes"` |
|
|
209
|
+
|
|
210
|
+
## WD7xx — Media & embeds
|
|
211
|
+
|
|
212
|
+
| Code | Error | Cause | Fix | Example |
|
|
213
|
+
| --- | --- | --- | --- | --- |
|
|
214
|
+
| `WD701` | Malformed `:video`/`:audio` | The line carries no clip source. | Give the clip path, then optional attributes and flags. | `:video /demo.mp4 controls` |
|
|
215
|
+
| `WD702` | Unknown `:video`/`:audio` flag | The bare flag is outside the supported set for that element. | Use `controls`, `autoplay`, `loop`, `muted`, or (video only) `playsinline`. | `:video /demo.mp4 controls` |
|
|
216
|
+
| `WD703` | Unknown `:video`/`:audio` attribute | The attribute is outside the supported set for that element. | Use `poster`, `width`, `height`, or `preload` (audio takes `preload` only). | `:video /demo.mp4 controls` |
|
|
217
|
+
| `WD704` | Malformed `:embed` | The line carries no URL. | Give the share or watch URL, then an optional title. | `:embed https://youtu.be/dQw4w9WgXcQ title="Demo"` |
|
|
218
|
+
| `WD705` | Unknown `:embed` attribute | The attribute is not `title`, `height`, `aspect`, or `allow`. | Remove the attribute, or use one of the supported ones. | `:embed https://youtu.be/dQw4w9WgXcQ title="Demo"` |
|
|
219
|
+
| `WD706` | Unknown `:embed allow` token | An `allow="…"` permission token is outside the fixed whitelist. | Use only the listed tokens; the message names every one. | `:embed https://youtu.be/dQw4w9WgXcQ title="Demo"` |
|
|
220
|
+
| `WD707` | Invalid `:embed` sizing | `aspect=` is not a `<w>:<h>` ratio, or `height=` is not a plain CSS length. | Write `aspect=4:3`, or a length like `height=400px`. | `:embed https://youtu.be/dQw4w9WgXcQ title="Demo"` |
|
|
221
|
+
|
|
222
|
+
## WD8xx — Skins & styling
|
|
223
|
+
|
|
224
|
+
| Code | Error | Cause | Fix | Example |
|
|
225
|
+
| --- | --- | --- | --- | --- |
|
|
226
|
+
| `WD801` | `scoped` is not the first line | The scoping opt-in appears somewhere other than the first meaningful line. | Move `scoped` to the top of the `.skin`, or delete it. | |
|
|
227
|
+
| `WD802` | Page-level declaration in a scoped skin | A declaration with no enclosing selector would write to the whole page. | Nest it under a selector, or move it to a global skin. | |
|
|
228
|
+
| `WD803` | Page-level selector in a scoped skin | A top-level `page`/`body`/`html`/`*`/`::selection` selector escapes the scope. | Move page-level styles to a global (non-scoped) skin. | |
|
|
229
|
+
| `WD804` | Malformed `@font-face` block | `@font-face` was given a name, holds a nested block, or has no descriptors — it takes plain `prop value` declaration lines only. | Write a bare `@font-face` line and indent each descriptor beneath it. | `@font-face`<br>` font-family "Newsreader"` |
|
|
230
|
+
| `WD805` | Invalid `@keyframes` name | The name after `@keyframes` is missing or is not a plain CSS identifier. | Name the animation with a plain identifier, e.g. `@keyframes fade-in`. | `@keyframes fade-in`<br>` to`<br>` opacity 1` |
|
|
231
|
+
| `WD806` | Invalid keyframe selector | A child of `@keyframes` is not `from`, `to`, or a percentage. | Use `from`, `to`, a percentage (`50%`), or a comma list of them (`50%, 75%`). | `@keyframes fade-in`<br>` to`<br>` opacity 1` |
|
|
232
|
+
| `WD807` | Malformed `@keyframes` body | A keyframe's declarations nest deeper, a keyframe selector has no declarations, or the block has no keyframe selectors at all. | Give each keyframe selector a flat list of declarations indented one level beneath it. | `@keyframes fade-in`<br>` to`<br>` opacity 1` |
|
|
233
|
+
|
|
234
|
+
## WD9xx — Project, routing & CLI
|
|
235
|
+
|
|
236
|
+
| Code | Error | Cause | Fix | Example |
|
|
237
|
+
| --- | --- | --- | --- | --- |
|
|
238
|
+
| `WD901` | Duplicate route | Two source files map to the same public route (often `.md` plus `.wd`). | Rename or delete one of the files. | |
|
|
239
|
+
| `WD902` | Route escapes the build output | A route resolved outside `dist`, which would write outside the build. | Remove traversal segments from the route. | |
|
|
240
|
+
| `WD903` | Unknown init template | `darkmown init --template` names a template that does not ship. | Pick a bundled template; the message lists every available name. | |
|
|
241
|
+
| `WD904` | Unknown deploy target | `darkmown deploy` names a platform Darkmown does not drive. | Deploy to `vercel` or `cloudflare`. | |
|
|
242
|
+
| `WD905` | Deploy CLI not signed in | The platform CLI rejected the deploy with an authentication failure. | Run the platform login command, then re-run the deploy. | |
|
|
243
|
+
| `WD906` | Deploy command failed | The platform CLI exited non-zero for a reason other than authentication. | Read the CLI output above the error, fix the cause, and re-run. | |
|
|
244
|
+
| `WD907` | Unknown `ai_crawlers` policy | The home page's `ai_crawlers:` is neither `allow` nor `deny`. | Write `ai_crawlers: allow` or `ai_crawlers: deny` (absent means allow). | |
|
|
245
|
+
| `WD908` | Unsafe route name | A page filename produced a route containing whitespace or control characters, which would corrupt generated config files (dist/_headers, llms.txt). | Rename the file using only letters, digits, dashes, and underscores. | |
|
|
246
|
+
| `WD950` | Invalid `rss_limit` | The home page's `rss_limit:` is not a positive whole number of items. | Write `rss_limit: 20` (absent means 20). | |
|
|
247
|
+
|
|
248
|
+
## Errors without a code
|
|
249
|
+
|
|
250
|
+
A handful of throws are framework invariants rather than authoring mistakes:
|
|
251
|
+
the expression re-parser (`src/compiler/expr-ast.js`) consumes the compiler's
|
|
252
|
+
own already-validated output, and the source reader (`src/compiler/reader.js`)
|
|
253
|
+
guards the host contract behind `compileFromMemory`. Hitting one of those is a
|
|
254
|
+
bug in Darkmown, not something a page can fix, so they stay uncoded and should
|
|
255
|
+
be reported as issues.
|
package/docs/spec-alignment.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This audit compares the current Darkmown implementation with the original MARROW/WhateverDown vision and the follow-up decisions made on 2026-06-11.
|
|
4
4
|
|
|
5
|
+
## 2.11.0 additions: the framework speaks MCP, and every page speaks markdown (2026-10-15)
|
|
6
|
+
|
|
7
|
+
- **The language description became a protocol surface.** `darkmown mcp` is a hand-rolled, zero-dependency MCP stdio server (newline-delimited JSON-RPC 2.0, protocol 2024-11-05, no SDK, so `dependencies` stays empty). Its five tools (catalog, grammar, in-memory validate, error-code lookup, doc-section search) are thin adapters over the same drift-guarded surfaces the CLI and the test suite already pin, so an agent editing `.wd` through MCP gets the same answers as `darkmown validate`, with the shipped markdown docs riding along as `darkmown://docs/*` resources. That resource surface is why `docs/errors.md` now ships in the tarball.
|
|
8
|
+
- **Any page can now be read as its source.** `md_source: true` on the home page publishes each route's byte-exact markdown at `<route>.md` with a `text/markdown` alternate link in the head, a per-page opt-out, drafts excluded by the same discovery filter as everything else, and no feed or sitemap pollution. The llms-full corpus already carried full page text for agents; the `.md` sources are the always-current, per-page form of the same contract, and they cost a plain file copy at build time.
|
|
9
|
+
|
|
5
10
|
## 2.10.1 additions: hardening and contract repair (2026-10-14)
|
|
6
11
|
|
|
7
12
|
A patch release: nothing in it moves the authoring surface, it tightens what the build publishes and repairs the error contract the 2.8.0 skin at-rules shipped without.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zvndev/darkmown",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"site",
|
|
67
67
|
"!site/_/og*.png",
|
|
68
68
|
"docs/cli.md",
|
|
69
|
+
"docs/errors.md",
|
|
69
70
|
"docs/spec-alignment.md",
|
|
70
71
|
"AGENTS.md",
|
|
71
72
|
"README.md",
|
package/site/pages/docs/index.wd
CHANGED
|
@@ -391,11 +391,12 @@ tags: [sales, revenue, "q1, q2"]
|
|
|
391
391
|
|
|
392
392
|
`lang:` sets the document language on `<html lang>` per page (`lang: fr`, `lang: pt-BR`, …); it defaults to `en`. Every page also ships a visually-hidden-until-focused **skip-to-content link** targeting the main landmark — your own `<main>` is reused (an existing `id` wins as the target; an id-less one gets `id="main"`), and a page without one is wrapped in `<main id="main">`. Restyle it via `.wd-skip-link` in a skin. All compile-time — static pages stay zero-JS.
|
|
393
393
|
|
|
394
|
-
|
|
394
|
+
More head and publishing keys arrived in 2.8 and 2.11:
|
|
395
395
|
|
|
396
396
|
- **`icon: /path`** sets the favicon verbatim (`<link rel="icon">`, with a `type` for `.svg`/`.png`/`.ico`). Without it, the first of `site/pages/icon.svg`, `icon.png`, or `favicon.ico` wins; without either, the built-in mark stays the fallback.
|
|
397
397
|
- **`noindex: true`** builds the page but removes it from discovery: a robots `noindex,nofollow` meta, plus exclusion from `sitemap.xml`, `rss.xml`, and the generated `llms.txt`/`llms-full.txt`. On a collection entry with a `_schema.wd`, declare it as `noindex: boolean?`.
|
|
398
398
|
- **`skins: [/base.skin]`** (an inline array) loads shared `.skin` files for that page only, resolved like `@include` paths; a missing file, a non-`.skin` file, or a path outside `site/pages`/`site/_` is `[WD017]`. A shared skin whose first line is `scoped` scopes to the whole page body, so keep to one scoped skin per page.
|
|
399
|
+
- **`md_source: true`** (home page only) publishes every page's byte-exact markdown source at `<route>.md` (`/docs/` also answers `/docs.md`, with a `<link rel="alternate" type="text/markdown">` head tag and a `text/markdown` content type). A page opts out with `md_source: false`; drafts never emit, and the sitemap and feeds never list the `.md` files. Off by default.
|
|
399
400
|
|
|
400
401
|
**Cascade order changed in 2.8.** Stylesheet links emit the highlight sheet first (code pages), then include skins, then `skins:` entries in listed order, and **the page's own colocated `.skin` last**, so the page wins ties at equal specificity. Before 2.8 the page's skin came first; a page rule that unknowingly relied on losing to an include rule now wins.
|
|
401
402
|
|
|
@@ -1021,6 +1022,10 @@ session.call("apply", { edits: [{ op: "replace", symbol: "state:count", text: ":
|
|
|
1021
1022
|
session.call("validate", {});
|
|
1022
1023
|
```
|
|
1023
1024
|
|
|
1025
|
+
### MCP server
|
|
1026
|
+
|
|
1027
|
+
`darkmown mcp` runs a hand-rolled, zero-dependency MCP server on stdio (protocol 2024-11-05), so an MCP-capable agent gets the language as tools instead of shelling out: `catalog`, `grammar`, `validate` (compile-check `.wd` source in memory with coded errors), `error` (a WD code's cause, fix, and example), and `doc` (fuzzy section search over the shipped README/AGENTS.md), plus the shipped docs as `darkmown://docs/{readme,agents,changelog,errors,cli}` resources. stdout is protocol frames only. Point a client at it with `{ "mcpServers": { "darkmown": { "command": "npx", "args": ["-y", "@zvndev/darkmown", "mcp"] } } }`.
|
|
1028
|
+
|
|
1024
1029
|
## Compile error codes
|
|
1025
1030
|
|
|
1026
1031
|
Every author-facing compile error opens with a stable `WDxxx` code, so it is searchable and matchable by tooling without parsing prose:
|
package/site/pages/index.wd
CHANGED
|
@@ -3,6 +3,7 @@ title: Darkmown: Markdown that runs
|
|
|
3
3
|
description: A .md file is a page. Rename it to .wd and the same file gets loops, state, fetched data, and forms, with no components, no JSX, and no build step. Pages with no state ship zero framework JavaScript.
|
|
4
4
|
image: https://darkmown.com/__wd/media/og.png
|
|
5
5
|
site_url: https://darkmown.com
|
|
6
|
+
md_source: true
|
|
6
7
|
transitions: true
|
|
7
8
|
schema: WebSite
|
|
8
9
|
---
|